import { GlLink } from '@gitlab/ui';
import { getAllByRole, getByTestId } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue';
import {
  SUPPORTING_MESSAGE_TYPES,
  VULNERABILITY_TRAINING_HEADING,
} from 'ee/vulnerabilities/constants';
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import ExplainThisVulnerability from 'ee/vulnerabilities/components/explain_this_vulnerability.vue';
import {
  REPORT_TYPE_API_FUZZING,
  REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
  REPORT_TYPE_CONTAINER_SCANNING,
  REPORT_TYPE_COVERAGE_FUZZING,
  REPORT_TYPE_DAST,
  REPORT_TYPE_DEPENDENCY_SCANNING,
  REPORT_TYPE_MANUALLY_ADDED,
  REPORT_TYPE_SAST,
  REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';

describe('Vulnerability Details', () => {
  let wrapper;

  const vulnerability = {
    id: 123,
    severity: 'bad severity',
    confidence: 'high confidence',
    reportType: 'Some report type',
    description: 'vulnerability description',
    descriptionHtml: 'vulnerability description <code>sample</code>',
  };

  const TEST_PROJECT_FULL_PATH = 'namespace/project';

  const createWrapper = (
    vulnerabilityOverrides,
    {
      mountFn = mount,
      explainVulnerability = false,
      mockVulnerabilityTrainingTemplate = false,
    } = {},
  ) => {
    const propsData = {
      vulnerability: { ...vulnerability, ...vulnerabilityOverrides },
    };
    wrapper = mountFn(VulnerabilityDetails, {
      propsData,
      provide: {
        projectFullPath: TEST_PROJECT_FULL_PATH,
        canViewFalsePositive: true,
        glFeatures: { explainVulnerability },
      },
      stubs: {
        VulnerabilityTraining: mockVulnerabilityTrainingTemplate
          ? { template: '<div><slot name="header"></slot></div>' }
          : true,
      },
    });
  };
  const createShallowWrapper = (vulnerabilityOverrides, options = {}) =>
    createWrapper(vulnerabilityOverrides, { mountFn: shallowMount, ...options });

  const getById = (id) => wrapper.find(`[data-testid="${id}"]`);
  const getAllById = (id) => wrapper.findAll(`[data-testid="${id}"]`);
  const getText = (id) => getById(id).text();
  const findVulnerabilityTraining = () => wrapper.findComponent(VulnerabilityTraining);
  const findFalsePositiveAlert = () => wrapper.findComponent(FalsePositiveAlert);
  const findExplainThisVulnerability = () => wrapper.findComponent(ExplainThisVulnerability);

  it('shows the properties that should always be shown', () => {
    createWrapper();
    expect(getById('description').html()).toContain(vulnerability.descriptionHtml);
    expect(wrapper.findComponent(SeverityBadge).props('severity')).toBe(vulnerability.severity);
    expect(getText('reportType')).toBe(`Tool: ${vulnerability.reportType}`);

    expect(getById('project').exists()).toBe(false);
    expect(findFalsePositiveAlert().exists()).toBe(false);
    expect(getById('title').exists()).toBe(false);
    expect(getById('image').exists()).toBe(false);
    expect(getById('os').exists()).toBe(false);
    expect(getById('file').exists()).toBe(false);
    expect(getById('class').exists()).toBe(false);
    expect(getById('method').exists()).toBe(false);
    expect(getById('url').exists()).toBe(false);
    expect(getById('evidence').exists()).toBe(false);
    expect(getById('scanner').exists()).toBe(false);
    expect(getAllById('link')).toHaveLength(0);
    expect(getAllById('identifier')).toHaveLength(0);
  });

  it.each([true, false])(
    'shows/hides the false-positive alert when `falsePositive` is: %s',
    (falsePositive) => {
      createWrapper({ falsePositive });
      expect(findFalsePositiveAlert().exists()).toBe(falsePositive);
    },
  );

  it('renders description when descriptionHtml is not present', () => {
    createWrapper({
      descriptionHtml: null,
    });
    expect(getById('description').html()).not.toContain(vulnerability.descriptionHtml);
    expect(getText('description')).toBe(vulnerability.description);
  });

  it('shows a link to the project if it exists', () => {
    const project = {
      fullName: 'myProject',
      fullPath: '/path/to/project',
    };
    createWrapper({ project });
    const projectLink = getById('project').findComponent(GlLink);

    expect(projectLink.attributes('href')).toBe(project.fullPath);
    expect(projectLink.text()).toBe(project.fullName);
  });

  it.each`
    reportType                  | expectedOutput
    ${'SAST'}                   | ${'SAST'}
    ${'DAST'}                   | ${'DAST'}
    ${'DEPENDENCY_SCANNING'}    | ${'Dependency Scanning'}
    ${'CONTAINER_SCANNING'}     | ${'Container Scanning'}
    ${'SECRET_DETECTION'}       | ${'Secret Detection'}
    ${'COVERAGE_FUZZING'}       | ${'Coverage Fuzzing'}
    ${'API_FUZZING'}            | ${'API Fuzzing'}
    ${'CLUSTER_IMAGE_SCANNING'} | ${'Cluster Image Scanning'}
  `(
    'displays "$expectedOutput" when report type is "$reportType"',
    ({ reportType, expectedOutput }) => {
      createWrapper({ reportType });
      expect(getText('reportType')).toBe(`Tool: ${expectedOutput}`);
    },
  );

  it('shows the title if it exists', () => {
    createWrapper({ title: 'some title' });
    expect(getText('title')).toBe('some title');
  });

  it('shows the location image if it exists', () => {
    createWrapper({ location: { image: 'some image' } });
    expect(getText('image')).toBe(`Image: some image`);
  });

  it('shows the operating system if it exists', () => {
    createWrapper({ location: { operatingSystem: 'linux' } });
    expect(getText('namespace')).toBe(`Namespace: linux`);
  });

  it('shows the vulnerability class if it exists', () => {
    createWrapper({ location: { file: 'file', class: 'class name' } });
    expect(getText('class')).toBe(`Class: class name`);
  });

  it('shows the crash state if it exists', () => {
    createWrapper({ location: { crashState: 'crash state' } });
    expect(getText('crash_state')).toBe(`Crash State: crash state`);
  });

  it('shows the vulnerability method if it exists', () => {
    createWrapper({ location: { vulnerableMethod: 'method name' } });
    expect(getText('method')).toBe(`Method: method name`);
  });

  it.each`
    description                       | vulnerabilityData
    ${"request's URL"}                | ${{ request: { url: 'http://host.test/foo/bar' } }}
    ${"location's hostname and path"} | ${{ location: { hostname: 'http://host.test', path: '/foo/bar' } }}
  `('shows the vulnerability URL when the $description is provided', ({ vulnerabilityData }) => {
    createWrapper({ ...vulnerabilityData });
    const expectedUrl = 'http://host.test/foo/bar';

    expect(getById('url').findComponent(GlLink).attributes('href')).toBe(expectedUrl);
    expect(getText('url')).toBe(`URL: ${expectedUrl}`);
  });

  // Remove once support for REST API's `location.method` is deprecated
  // https://gitlab.com/groups/gitlab-org/-/epics/3657 and https://gitlab.com/groups/gitlab-org/-/epics/8054
  it('shows the vulnerability method if it exists for `location.method`', () => {
    createWrapper({ location: { method: 'method name' } });
    expect(getText('method')).toBe(`Method: method name`);
  });

  it('shows the crash type if it exists', () => {
    createWrapper({ location: { crashType: 'crash type' } });
    expect(getText('crash_type')).toBe(`Crash Type: crash type`);
  });

  it('shows the evidence if it exists', () => {
    createWrapper({ evidence: 'some evidence' });
    expect(getText('evidence')).toBe(`Evidence: some evidence`);
  });

  it('shows the links if they exist', () => {
    const links = [
      { url: 'http://foo.bar/1' },
      { url: 'http://foo.bar/2', name: 'link 2' },
      { url: 'http://foo.bar/3' },
    ];
    createWrapper({ links });

    const linkEls = getAllById('link');
    expect(linkEls).toHaveLength(links.length);

    linkEls.wrappers.forEach((link, index) => {
      const { url, name } = links.at(index);

      expect(link.attributes()).toMatchObject({
        target: '_blank',
        href: url,
      });
      expect(link.text()).toBe(name || url);
    });
  });

  it('shows the vulnerability identifiers if they exist', () => {
    const identifiersData = [
      { name: '00', url: 'http://example.com/00' },
      { name: '11', url: 'http://example.com/11' },
      { name: '22', url: 'http://example.com/22' },
      { name: '33' },
      { name: '44' },
      { name: '55' },
    ];

    createWrapper({
      identifiers: identifiersData,
    });

    const identifiers = getAllById('identifier');

    expect(identifiers).toHaveLength(identifiersData.length);

    const checkIdentifier = ({ name, url }, index) => {
      const identifier = identifiers.at(index);

      expect(identifier.text()).toBe(name);

      if (url) {
        expect(identifier.is(GlLink)).toBe(true);
        expect(identifier.attributes()).toMatchObject({
          target: '_blank',
          href: url,
        });
      } else {
        expect(identifier.is(GlLink)).toBe(false);
      }
    };

    identifiersData.forEach(checkIdentifier);
  });

  it('shows the vulnerability assets if they exist', () => {
    const assetsData = [
      { name: 'Postman Collection', url: 'http://example.com/postman' },
      { name: 'HTTP Messages', url: 'http://example.com/http-messages' },
      { name: 'Foo' },
      { name: 'Bar' },
    ];

    createWrapper({
      assets: assetsData,
    });

    const assets = getAllById('asset');

    expect(assets).toHaveLength(assetsData.length);

    const checkIdentifier = ({ name, url }, index) => {
      const asset = assets.at(index);

      expect(asset.text()).toBe(name);

      if (url) {
        expect(asset.is(GlLink)).toBe(true);
        expect(asset.attributes()).toMatchObject({
          target: '_blank',
          href: url,
        });
      } else {
        expect(asset.is(GlLink)).toBe(false);
      }
    };

    assetsData.forEach(checkIdentifier);
  });

  describe('VulnerabilityTraining', () => {
    const identifiers = [{ externalType: 'cwe', externalId: 'cwe-123' }];
    const location = { file: 'test.txt' };

    it('renders component', () => {
      createShallowWrapper({ identifiers, location });

      expect(findVulnerabilityTraining().props()).toMatchObject({
        identifiers,
        projectFullPath: TEST_PROJECT_FULL_PATH,
        file: location.file,
      });
    });

    it('renders title text', () => {
      createShallowWrapper({ identifiers }, { mockVulnerabilityTrainingTemplate: true });

      expect(wrapper.text()).toContain(VULNERABILITY_TRAINING_HEADING.title);
    });

    it('passes a null file prop if the location is undefined', () => {
      createShallowWrapper({ identifiers });
      expect(findVulnerabilityTraining().props('file')).toBeNull();
    });
  });

  describe('file link', () => {
    const file = () => getById('file').findComponent(GlLink);

    it('shows only the file name if there is no start line', () => {
      createWrapper({ location: { file: 'test.txt', blobPath: 'blob_path.txt' } });
      expect(file().attributes('target')).toBe('_blank');
      expect(file().attributes('href')).toBe('blob_path.txt');
      expect(file().text()).toBe('test.txt');
    });

    it('shows the correct line number when there is a start line', () => {
      createWrapper({ location: { file: 'test.txt', startLine: 24, blobPath: 'blob.txt' } });
      expect(file().attributes('target')).toBe('_blank');
      expect(file().attributes('href')).toBe('blob.txt#L24');
      expect(file().text()).toBe('test.txt:24');
    });

    it('shows the correct line numbers when there is a start and end line', () => {
      createWrapper({
        location: { file: 'test.txt', startLine: 24, endLine: 27, blobPath: 'blob.txt' },
      });
      expect(file().attributes('target')).toBe('_blank');
      expect(file().attributes('href')).toBe('blob.txt#L24-27');
      expect(file().text()).toBe('test.txt:24-27');
    });

    it('shows only the start line when the end line is the same', () => {
      createWrapper({
        location: { file: 'test.txt', startLine: 24, endLine: 24, blobPath: 'blob.txt' },
      });
      expect(file().attributes('target')).toBe('_blank');
      expect(file().attributes('href')).toBe('blob.txt#L24');
      expect(file().text()).toBe('test.txt:24');
    });
  });

  describe('scanner', () => {
    const link = () => getById('scannerSafeLink');
    const scannerText = () => getById('scanner').text();

    it('shows the scanner name only but no link', () => {
      createWrapper({ scanner: { name: 'some scanner' } });
      expect(scannerText()).toBe('Scanner: some scanner');
      expect(link().element instanceof HTMLSpanElement).toBe(true);
    });

    it('shows the scanner name and version but no link', () => {
      createWrapper({ scanner: { name: 'some scanner', version: '1.2.3' } });
      expect(scannerText()).toBe('Scanner: some scanner (version 1.2.3)');
      expect(link().element instanceof HTMLSpanElement).toBe(true);
    });

    it('shows the scanner name only with a link', () => {
      createWrapper({ scanner: { name: 'some tool', url: '//link' } });
      expect(scannerText()).toBe('Scanner: some tool');
      expect(link().attributes('href')).toBe('//link');
    });

    it('shows the scanner name and version with a link', () => {
      createWrapper({ scanner: { name: 'some tool', version: '1.2.3', url: '//link' } });
      expect(scannerText()).toBe('Scanner: some tool (version 1.2.3)');
      expect(link().attributes('href')).toBe('//link');
    });
  });

  describe('http data', () => {
    const TEST_HEADERS = [
      { name: 'Name1', value: 'Value1' },
      { name: 'Name2', value: 'Value2' },
    ];
    const EXPECT_REQUEST = {
      label: 'Sent request:',
      content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
      isCode: true,
    };

    const EXPECT_REQUEST_WITHOUT_BODY = {
      label: 'Sent request:',
      content:
        'GET http://www.gitlab.com\nName1: Value1\nName2: Value2\n\n<Message body is not provided>',
      isCode: true,
    };

    const EXPECT_REQUEST_WITH_EMPTY_STRING = {
      label: 'Sent request:',
      content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2',
      isCode: true,
    };

    const EXPECT_RESPONSE = {
      label: 'Actual response:',
      content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
      isCode: true,
    };

    const EXPECT_RESPONSE_WITHOUT_REASON_PHRASE = {
      label: 'Actual response:',
      content: '500 \nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
      isCode: true,
    };

    const EXPECT_RESPONSE_WITHOUT_BODY = {
      label: 'Actual response:',
      content:
        '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n<Message body is not provided>',
      isCode: true,
    };

    const EXPECT_RESPONSE_WITH_EMPTY_STRING = {
      label: 'Actual response:',
      content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2',
      isCode: true,
    };

    const EXPECT_RECORDED_RESPONSE = {
      label: 'Unmodified response:',
      content: '200 OK\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
      isCode: true,
    };

    const EXPECT_RECORDED_RESPONSE_WITHOUT_REASON_PHRASE = {
      label: 'Unmodified response:',
      content: '200 \nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
      isCode: true,
    };

    const EXPECT_RECORDED_RESPONSE_WITHOUT_BODY = {
      label: 'Unmodified response:',
      content: '200 OK\nName1: Value1\nName2: Value2\n\n<Message body is not provided>',
      isCode: true,
    };

    const EXPECT_RECORDED_RESPONSE_WITH_EMPTY_STRING = {
      label: 'Unmodified response:',
      content: '200 OK\nName1: Value1\nName2: Value2',
      isCode: true,
    };

    const getTextContent = (el) => el.textContent.trim();
    const getLabel = (el) => getTextContent(getByTestId(el, 'label'));
    const getContent = (el) => getTextContent(getByTestId(el, 'value'));
    const getSectionData = (testId) => {
      const section = getById(testId).element;

      if (!section) {
        return null;
      }

      return getAllByRole(section, 'listitem').map((li) => ({
        label: getLabel(li),
        content: getContent(li),
        ...(li.querySelector('code') ? { isCode: true } : {}),
      }));
    };

    it.each`
      request                                                                                             | expectedData
      ${null}                                                                                             | ${null}
      ${{}}                                                                                               | ${null}
      ${{ headers: TEST_HEADERS }}                                                                        | ${null}
      ${{ method: 'GET' }}                                                                                | ${null}
      ${{ method: 'GET', url: 'http://www.gitlab.com' }}                                                  | ${null}
      ${{ method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }}                        | ${null}
      ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${[EXPECT_REQUEST]}
      ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: null }}               | ${[EXPECT_REQUEST_WITHOUT_BODY]}
      ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: undefined }}          | ${[EXPECT_REQUEST_WITHOUT_BODY]}
      ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '' }}                 | ${[EXPECT_REQUEST_WITH_EMPTY_STRING]}
    `('shows request data for $request', ({ request, expectedData }) => {
      createWrapper({ request });
      expect(getSectionData('request')).toEqual(expectedData);
    });

    it.each`
      response                                                                                                         | expectedData
      ${null}                                                                                                          | ${null}
      ${{}}                                                                                                            | ${null}
      ${{ headers: TEST_HEADERS }}                                                                                     | ${null}
      ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]' }}                                                           | ${null}
      ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500' }}                                        | ${[EXPECT_RESPONSE_WITHOUT_REASON_PHRASE]}
      ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE]}
      ${{ headers: TEST_HEADERS, body: null, statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }}               | ${[EXPECT_RESPONSE_WITHOUT_BODY]}
      ${{ headers: TEST_HEADERS, body: undefined, statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }}          | ${[EXPECT_RESPONSE_WITHOUT_BODY]}
      ${{ headers: TEST_HEADERS, body: '', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }}                 | ${[EXPECT_RESPONSE_WITH_EMPTY_STRING]}
    `('shows response data for $response', ({ response, expectedData }) => {
      createWrapper({ response });
      expect(getSectionData('response')).toEqual(expectedData);
    });

    it.each`
      supportingMessages                                                                                                                                         | expectedData
      ${null}                                                                                                                                                    | ${null}
      ${[]}                                                                                                                                                      | ${null}
      ${[{}]}                                                                                                                                                    | ${null}
      ${[{}, { response: {} }]}                                                                                                                                  | ${null}
      ${[{}, { response: { headers: TEST_HEADERS } }]}                                                                                                           | ${null}
      ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]' } }]}                                                                                 | ${null}
      ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200' } }]}                                                             | ${null}
      ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200', reason_phrase: 'OK' } }]}                                        | ${null}
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '200' } }]}                     | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_REASON_PHRASE]}
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE]}
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: null, statusCode: '200', reasonPhrase: 'OK' } }]}               | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]}
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: undefined, statusCode: '200', reasonPhrase: 'OK' } }]}          | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]}
      ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '', statusCode: '200', reasonPhrase: 'OK' } }]}                 | ${[EXPECT_RECORDED_RESPONSE_WITH_EMPTY_STRING]}
    `('shows response data for $supporting_messages', ({ supportingMessages, expectedData }) => {
      createWrapper({ supportingMessages });
      expect(getSectionData('recorded-response')).toEqual(expectedData);
    });
  });

  describe('Explain this vulnerability feature', () => {
    it.each`
      reportType                            | isShown  | explainVulnerability
      ${REPORT_TYPE_SAST}                   | ${true}  | ${true}
      ${REPORT_TYPE_SAST}                   | ${false} | ${false}
      ${REPORT_TYPE_DAST}                   | ${false} | ${true}
      ${REPORT_TYPE_SECRET_DETECTION}       | ${false} | ${true}
      ${REPORT_TYPE_DEPENDENCY_SCANNING}    | ${false} | ${true}
      ${REPORT_TYPE_CONTAINER_SCANNING}     | ${false} | ${true}
      ${REPORT_TYPE_CLUSTER_IMAGE_SCANNING} | ${false} | ${true}
      ${REPORT_TYPE_COVERAGE_FUZZING}       | ${false} | ${true}
      ${REPORT_TYPE_API_FUZZING}            | ${false} | ${true}
      ${REPORT_TYPE_MANUALLY_ADDED}         | ${false} | ${true}
    `(
      'shows the Explain This Vulnerability component for report type $reportType? $isShown',
      ({ reportType, isShown, explainVulnerability }) => {
        createShallowWrapper({ reportType }, { explainVulnerability });

        expect(findExplainThisVulnerability().exists()).toBe(isShown);
      },
    );
  });
});
